import torch
from torch.autograd import Variable

"""
PyTorch:定义新的 autograd 函数

在底层，每个原始 autograd 操作符实际上是在 Tensors 上操作的两个函数。forward 函数从输入张量来计算输出张量。
backward函数接收输出向量关于某个标量的梯度，然后计算输入张量关于关于同一个标量的梯度。
在 PyTorch 中， 我们可以通过定义一个 torch.autograd.Function 的子类并实现 forward 和 backward两个函数容易地实现我们自己的 autograd 操作符。
我们可以通过定义一个该操作符的实例，并像函数一样给它传递一个包含输入数据的 Variable 来调用它，这样就使用我们新定义的 autograd 操作符。
在这个例子中我们， 我们自己定义了一个 autograd 函数来执行 ReLU 非线性映射，并使用它实现了一个两层的网络：
"""

# 定义relu类
class MyReLU(torch.autograd.Function):
    """
    我们可以基于基于子方法torch.autograd.Function实现我们自己定制的autograd函数
    实现操作在张量上的正向传播和反向传播传递
    We can implement our own custom autograd Function by subclassing
    torch.autograd.Function and implementing forward and backward passed
    which operate on tensors
    """

    # 正向传播
    def forward(self, input):
        """
        # 在正向传播过程中我们收到一个保护输入的张量并且返回一个保护输出的张量
        # 我们可以使用save_for_backward方法缓存任何一个张量用于反向传播
        In the forward pass we receive a  Tensor containing the input and return a
        Tensor containing the output. You can cache arbitrary Tensors for use in the
        backward pass using the save_for_backward method.
        """
        self.save_for_backward(input)
        return input.clamp(min=0)

    # 反向传播
    def backward(self, grad_output):
        """
        # 在反向传播过程中我们收到一个包含对于输出损失梯度的张量，我们需要计算对于输入的损失梯度
        In the backward pass we receive a Tensor containing the gradient of loss
        with respect to the output, and we need to compute the gradient of the loss
        with respect to the input
        """

        input, = self.saved_tensors
        grad_input = grad_output.clone()
        grad_input[input < 0] = 0
        return grad_input

# 定义Tensor类型
dtype = torch.FloatTensor
# dtype = torch.cuda.FloatTensor # Uncomment this to run on GPU

# N 批的大小
# D_in 输入的数据纬度
# H 隐藏层纬度
# D_out 输出数据纬度
# N is batch size; D_in is input dimenssion
# H is hidden demenssion; D_out is output dimension
N, D_in, H, D_out = 64, 1000, 100, 10

# 创建随机的张量来保存输入和输出，并把它们放在变量里
# 设置requires_grad=False，表示我们在反向传播时不需要计算它们的梯度
# Create random Tensors to hold input and outpus, and wrap them in Variables.
# Setting requires_grad=False indicates that we do not need to compute gradients
# with respect to these Variables during the backward pass
x = Variable(torch.randn(N, D_in).type(dtype), requires_grad=False)
y = Variable(torch.randn(N, D_out).type(dtype), requires_grad=False)

# 创建随机的张量来保存输入和输出，并把它们放在变量里
# 设置requires_grad=True，表示我们在反向传播时要计算它们的梯度
# Create random Tensors for weights, and wrap them in Variables.
# Setting requires_grad=True indicates that we want to compute gradients with
# respect to these Variables during the backward pass.
w1 = Variable(torch.randn(D_in, H).type(dtype), requires_grad=True)
w2 = Variable(torch.randn(H, D_out).type(dtype), requires_grad=True)

# 学习率
learning_rate = 1e-6

# 开始学习
for t in range(500):
    # 构建MyRelU类在我们的网络中使用
    # Construct an instance of our MyReLU class to use in our network
    relu = MyReLU()

    # 正向传播： 使用变量运算计算预测的y，我们使用ReLU我们定制的autograd运算
    # Forward pass: compute predicted y using operations on Variables; we compute
    # ReLU using our custom autograd operation.
    y_pred = relu(x.mm(w1)).mm(w2)

    # 使用变量运算计算和打印损失，现在损失是一个形状(1,)的变量，
    # loss.data是形状(1,)的张量，loss.data[0]是保存损失的标量
    # Compute and print loss using operations on Variables.
    # Now loss is a Variable of shape (1,) and loss.data is a Tensor of shape
    # (1,); loss.data[0] is a scalar value holding the loss.
    loss = (y_pred - y).pow(2).sum()
    print(t, loss.data[0])

    # 使用autaograd计算反向传播，调用这个方法会为requires_grad=True的变量计算损失梯度
    # 调用后w1.grad和w2.grad将会分别保存w1和w2的梯度
    # Use autograd to compute the backward pass. This call will compute the
    # gradient of loss with respect to all Variables with requires_grad=True.
    # After this call w1.grad and w2.grad will be Variables holding the gradient
    # of the loss with respect to w1 and w2 respectively.
    loss.backward()

    # 使用梯度更新权重，w1.data和w2.data是张量，w1.grad和w2.grad是变量
    # w1.grad.data和w2.grad.data是张量
    # Update weights using gradient descent; w1.data and w2.data are Tensors,
    # w1.grad and w2.grad are Variables and w1.grad.data and w2.grad.data are
    # Tensors.
    w1.data -= learning_rate * w1.grad.data
    w2.data-= learning_rate * w2.grad.data

    # 更新权重后手动清零梯度
    # Manually zero the gradients after updating weights
    w1.grad.data.zero_()
    w2.grad.data.zero_()